Skip to content

📑 PR: feat(bitflow-funding-coordinator): add funding coordinator skill#585

Merged
diegomey merged 6 commits into
BitflowFinance:mainfrom
macbotmini-eng:macbotmini-eng/bitflow-funding-coordinator
May 15, 2026
Merged

📑 PR: feat(bitflow-funding-coordinator): add funding coordinator skill#585
diegomey merged 6 commits into
BitflowFinance:mainfrom
macbotmini-eng:macbotmini-eng/bitflow-funding-coordinator

Conversation

@macbotmini-eng
Copy link
Copy Markdown
Contributor

@macbotmini-eng macbotmini-eng commented May 5, 2026

Skill Submission

Skill name: bitflow-funding-coordinator
Category: Trading / Infrastructure
HODLMM integration? No direct HODLMM write. This is the upstream funding leg for HODLMM/Zest strategies.

What it does

Adds bitflow-funding-coordinator, a composed write skill for the funding leg described in #584.

The skill turns a caller-selected source token into a route-ready target token through the accepted bitflow-swap-aggregator, records a local checkpoint, confirms the funding txid on Hiro, and emits a handoff payload for downstream strategy skills such as bitflow-hodlmm-zest-yield-loop.

V1 is shaped around the #471 / #559 funding path:

STX -> sBTC -> ready for #559

This PR intentionally stops after funding. It does not deposit to HODLMM, supply to Zest, borrow, repay, unwind, or choose a yield venue.

Closes #584.

Files

  • skills/bitflow-funding-coordinator/SKILL.md
  • skills/bitflow-funding-coordinator/AGENT.md
  • skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts

Implementation notes

  • Delegates quote/plan/run execution to bitflow-swap-aggregator; no source imports from another skill directory.
  • Adds coordinator-level doctor, status, plan, run, resume, and cancel commands.
  • Requires --confirm=FUND before any funding write.
  • Delegates the primitive write with the swap primitive's own --confirm=SWAP gate.
  • Writes checkpoints under the standard AIBTC state path for this skill.
  • Refuses overlapping unresolved local checkpoints.
  • Records a returned txid before the coordinator's Hiro confirmation loop.
  • Emits explicit boundary flags showing that no HODLMM, Zest, borrow, or leverage write happened.

On-chain proof

Mainnet proof landed 2026-05-08T08:45:20Z UTC.

TX: https://explorer.hiro.so/txid/0x766e90d35cba7b9a9f9c5f7aa5416c1facad65b1cc2541fc9c06c50116c16fa2?chain=mainnet

  • Sender: SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8 (matches --wallet and Hiro sender_address)
  • Path: token-stx → token-sbtc via Velar UNIV2V2 (SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.wrapper-velar-path-v-1-2.swap-univ2v2)
  • Amounts: 1 STX → (ok u325) (325 sats sBTC, deny mode + 4 PCs intact, fee 70,000 µSTX)
  • Status: Hiro tx_status: success, canonical, vm_error: null, block 7897446 / burn 948436

Plan command:

bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts plan \
  --wallet SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8 \
  --token-in token-stx --token-out token-sbtc --amount-in 1 \
  --max-slippage-bps 100 --handoff-label bitflow-hodlmm-zest-yield-loop

Run command:

bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts run \
  --wallet SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8 \
  --token-in token-stx --token-out token-sbtc --amount-in 1 \
  --max-slippage-bps 100 --handoff-label bitflow-hodlmm-zest-yield-loop \
  --confirm=FUND --wait-seconds 300

No HODLMM / Zest / borrow / downstream write: run envelope emitted boundaries.hodlmmWritePerformed: false, boundaries.zestWritePerformed: false, boundaries.borrowOrLeveragePerformed: false, boundaries.downstreamWritesPerformed: false.

PRD safety guards (resume path) exercised — read-only, no second broadcast:

  • happy path against proof tx → status: success; proof.sender matches --wallet; proof.function: swap-univ2v2 in allowlist; proof.status: success (PRD §9 + §13)
  • RESUME_SENDER_MISMATCH (mismatched --wallet) → status: blocked (PRD §9 negative)
  • RESUME_TX_NOT_SWAP against https://explorer.hiro.so/txid/0x73d7027ee5e072a8bfb52df56c85bb0dd156bed871b04621dbb5b0a158ca2d43?chain=mainnet (add-relative-liquidity-same-multi on dlmm-liquidity-router-v-1-1 — the function arc0btc removed from EXPECTED_SWAP_FUNCTIONS at commit 453abff) → status: blocked (PRD §13 negative + arc0btc fix verification)
  • RESUME_REQUIRES_TOKEN_OUT (no checkpoint, no --token-out) → status: blocked (PRD §13 synthesis guard)

Registry compatibility checklist

  • SKILL.md uses metadata: nested frontmatter
  • AGENT.md starts with YAML frontmatter (name, skill, description)
  • tags and requires are comma-separated quoted strings
  • user-invocable is the string "false"
  • entry path is repo-root-relative with no skills/ prefix
  • metadata.author is macbotmini-eng
  • Commands output JSON to stdout
  • Error/blocker output uses structured JSON

Smoke test results

doctor output summary
{
  "status": "success",
  "action": "doctor",
  "data": {
    "network": "mainnet",
    "wallet": "SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8",
    "hiro": { "ok": true, "chainId": 1 },
    "dependencies": {
      "bitflowSwapAggregator": true,
      "nonceManagerDeclared": true
    },
    "unresolvedCheckpoint": false,
    "primitive": {
      "status": "success",
      "action": "doctor",
      "data": {
        "bitflowSdk": {
          "getAvailableTokens": true,
          "getQuoteForRoute": true,
          "prepareSwap": true,
          "getSwapParams": true
        },
        "tokenCount": 202,
        "walletChecks": {
          "pendingDepth": 0
        }
      }
    }
  },
  "error": null
}
plan output summary
{
  "status": "success",
  "action": "plan",
  "data": {
    "fundingRoute": "token-stx-to-token-sbtc",
    "mode": "one-shot",
    "wallet": "SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8",
    "tokenIn": "token-stx",
    "tokenOut": "token-sbtc",
    "amountIn": "1",
    "expectedAmountOut": "0.00000279",
    "routeReady": false,
    "handoff": {
      "label": "bitflow-hodlmm-zest-yield-loop",
      "readyToken": "token-sbtc",
      "readyAmount": null,
      "routeReady": false
    },
    "primitive": {
      "status": "success",
      "action": "plan",
      "data": {
        "quote": {
          "tokenPath": ["token-stx", "token-sbtc"],
          "dexPath": ["VELAR_UNIV2V2_PATH"],
          "swapFunction": "swap-univ2v2"
        },
        "execution": {
          "postConditionMode": "deny",
          "postConditionCount": 4
        },
        "safety": {
          "pendingDepth": 0,
          "mempoolDepthLimit": 0
        }
      }
    }
  },
  "error": null
}
confirmation gate output
{
  "status": "blocked",
  "action": "run",
  "data": {},
  "error": {
    "code": "CONFIRMATION_REQUIRED",
    "message": "This write skill requires --confirm=FUND.",
    "next": "Review plan output and rerun with --confirm=FUND."
  }
}

Validation

  • bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts --help passed.
  • bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts doctor --wallet SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8 returned success.
  • bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts plan --wallet SP2G6TM8JCRNK6WSPQE8S86FP2W3A4FEVGZCCCQT8 --token-in token-stx --token-out token-sbtc --amount-in 1 --max-slippage-bps 100 --handoff-label bitflow-hodlmm-zest-yield-loop returned success.
  • bun run skills/bitflow-funding-coordinator/bitflow-funding-coordinator.ts run ... without --confirm=FUND returned CONFIRMATION_REQUIRED.
  • bun run scripts/generate-manifest.ts --dry-run includes bitflow-funding-coordinator.
  • bun run scripts/validate-frontmatter.ts reports bitflow-funding-coordinator passed with 0 errors and 0 warnings. Full-repo validation still reports pre-existing unrelated failures in dca, stacking-delegation, and zest-yield-manager.
  • git diff --check passed.

Security notes

  • Write skill; can move funds when run --confirm=FUND is used.
  • Mainnet only.
  • No secret material or local runtime internals are hardcoded.
  • Uses local checkpointing to prevent blind retry after interruption.
  • Emits explicit boundary flags so callers do not mistake funding for downstream HODLMM/Zest deployment.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

✅ Validation Passed

Skill: bitflow-funding-coordinator
Errors: 0
Warnings: 0

All checks passed. This submission is ready for review.

@TheBigMacBTC TheBigMacBTC marked this pull request as ready for review May 5, 2026 09:54
@BitflowFinance BitflowFinance locked and limited conversation to collaborators May 5, 2026
@TheBigMacBTC TheBigMacBTC added Bitflow Bitflow Primitive Primitive labels May 5, 2026
Copy link
Copy Markdown
Contributor

@diegomey diegomey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Composition pattern is clean — subprocess via spawn, no source imports, primitive output parsed as JSON, txid persisted to checkpoint before Hiro polling (PRD safety req #7 satisfied). --confirm=FUND enforced, --confirm=SWAP correctly delegated to the primitive automatically. runFunding passes waitSeconds: "0" to the primitive so the coordinator owns the polling loop, which is the right division of labor. boundaries object explicitly emitted (hodlmmWritePerformed: false, etc.) — good for downstream auditors. AGENT.md, SKILL.md, and the CLI surface match the PRD shape. Validation passes.

That said, several gaps against PRD safety requirements warrant changes before merge.

Blocking

1. nonce-manager declared but never invoked. PRD safety req #6 is explicit: "Nonce-manager must serialize write execution: acquire → write → release." metadata.requires lists nonce-manager, dependencySignals() reports nonceManagerDeclared: true, but no acquire/release call exists anywhere in the code. The local checkpoint via isUnresolved() only serializes within a single process for a single wallet — it doesn't coordinate across multiple skill invocations or other writers competing for the same sender nonce. nonce state is also missing from the checkpoint schema, which the PRD specifies as a required field.

2. resume does not verify sender matches --wallet. PRD safety req #9 is explicit: "resume --txid must verify sender and Hiro tx_status: success." txProof() extracts sender_address from the Hiro response but runResume() never compares it to wallet. Combined with #3 below, this is a real safety hole.

3. resume synthesizes a checkpoint with tokenIn/tokenOut: "unknown" when no local state exists, and never verifies the on-chain tx actually swapped to tokenOut. Someone calling resume --wallet X --txid <some-success-tx> with no prior checkpoint gets a synthesized checkpoint, marked complete, with routeReady: true and handoff.readyToken: <whatever-was-passed>. The on-chain contract_call.function_name is captured in proof but never validated against the expected swap shape. Combined with #2, a downstream consumer could be handed a routeReady: true payload pointing at a tx that didn't produce the claimed token, from a sender that isn't --wallet.

Meaningful gaps (fix, not blocking)

4. txid and hiroStatus not at top level of the success envelope. PRD output contract specifies them as top-level keys in data. Implementation buries them in nested proof and checkpoint objects. AGENT.md own guardrail says "Always surface txid, Hiro status..." — internally inconsistent with the actual output shape. Downstream consumers reading per the PRD spec will miss them.

5. --target-out accepted but never enforced. PRD lists it as "Desired minimum target-token output." Current code accepts the flag, stores it in checkpoint.targetOut, and never compares it against the actual primitive output. If the user specifies --target-out 0.001 and the swap returns 0.0005, run completes with routeReady: true.

6. No coordinator-level mempool check. --mempool-depth-limit is passed through to the primitive only. PRD safety req #4 frames this as a coordinator-level gate. Defensible delegation, but the PRD lists it under coordinator responsibilities, not primitive ones.

7. No tokenOut verification against the primitive plan output. Coordinator trusts the primitive's tokenPath without checking the last element matches --token-out. The smoke test shows ["token-stx", "token-sbtc"] correctly, but this isn't asserted programmatically. PRD safety req #13: "Reject routes that do not produce the requested target token."

8. status doesn't surface source/target balances. PRD: "Read funding posture: source balance, target balance, pending checkpoint." Implementation returns the primitive's doctor output (which includes walletChecks.pendingDepth but no balances) and the local checkpoint. Operator can't see balances from status output without running doctor separately on the primitive.

Minor

  • fail() calls process.exit(0) on BlockedError but process.exitCode = 1 on other errors. Intentional (blocked is recoverable) but unusual; downstream agents keying on exit codes need to know. Worth a comment in the file header.
  • --mode dca-chunk is accepted as a flag but follows the same code path as one-shot. PRD permits deferring DCA, but if the flag is exposed it should at least be validated or echo a "not yet supported" notice rather than silently behaving identically.
  • extractExpectedOutput() walks the primitive output for the first key named quote or expectedAmountOut. Fragile; depends on first-match-wins traversal of the primitive's JSON shape.
  • --mode value is not validated (--mode banana is accepted and stored).
  • --slippage-bps alias for --max-slippage-bps is fine, but the PRD only specifies --max-slippage-bps. Consider documenting the alias in SKILL.md or removing it.

Recommendation

Items 1, 2, and 3 should land before merge — they are explicit PRD safety requirements with real consequences (nonce coordination across writers; sender impersonation in resume). Items 4–8 can be follow-ups if shipping sooner is preferred. Proof is correctly noted as Pending and the PR is intentionally pre-proof per its own draft gate, so that's not a blocker on this review.

…ocking items

Three blocking items from Diego CHANGES_REQUESTED review at
BitflowFinance#585 (review)

1. nonce-manager actually invoked (not just declared).
   New helpers acquireNonce(wallet) + releaseNonce(wallet, nonce, outcome)
   shell out to skills/nonce-manager/nonce-manager.ts. runFunding now:
   - acquireNonce BEFORE primitive.run broadcast
   - releaseNonce success on confirmed success
   - releaseNonce failed when broadcast but Hiro non-success (nonce IS
     consumed per nonce-manager spec)
   - releaseNonce rejected when broadcast itself threw or txid missing
     (nonce NOT consumed, safe to roll back)
   Checkpoint schema gains nonce + nonceState fields per PRD shape req.
   PRD safety req BitflowFinance#6: 'Nonce-manager must serialize write execution.'

2. resume verifies sender matches --wallet.
   Hiro response sender_address now compared against --wallet; refuses
   with RESUME_SENDER_MISMATCH on mismatch. PRD safety req BitflowFinance#9:
   'resume --txid must verify sender and Hiro tx_status: success.'

3. resume verifies on-chain tx is actually a Bitflow swap.
   New EXPECTED_SWAP_FUNCTIONS allowlist (swap-helper-a/b, swap-univ2v2,
   swap-x-for-y, swap-y-for-x, add-relative-liquidity-same-multi).
   contract_call.function_name compared against allowlist; refuses with
   RESUME_TX_NOT_SWAP if mismatch. Prevents synthesizing routeReady:true
   on success txids from unrelated wallet operations. PRD safety req BitflowFinance#13:
   'Reject routes that do not produce the requested target token.'
   Combined with the synthesis guard requiring explicit --token-out when
   no local checkpoint exists (RESUME_REQUIRES_TOKEN_OUT) — operator
   intent must be grounded, not defaulted.

4. (meaningful gap 4) txid + hiroStatus surfaced at top-level of run +
   resume success envelopes per PRD output contract; previously buried
   in nested proof + checkpoint objects, contradicting AGENT.md.

Validation:
- bun build --no-bundle: PASS
- bun run scripts/validate-frontmatter.ts: 0 errors, 0 warnings
- diff scope: only skills/bitflow-funding-coordinator/

Items 5-8 + minor notes deferred to follow-ups per Diego's
'shipping sooner is preferred' framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@BitflowFinance BitflowFinance unlocked this conversation May 5, 2026
@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@diegomey @arc0btc @TheBigMacBTC — Diego review at #585 (review) blocking items addressed in commit ec36cc0d.

Item 1 — nonce-manager actually invoked. New acquireNonce(wallet) + releaseNonce(wallet, nonce, outcome) helpers shell out to skills/nonce-manager/nonce-manager.ts. runFunding acquires the nonce-manager file-lock BEFORE primitive broadcast, releases as success / failed (broadcast-but-not-success — nonce IS consumed per nonce-manager spec) / rejected (broadcast threw or txid missing — nonce safe to roll back). Checkpoint schema gains nonce + nonceState fields per PRD shape req. PRD safety req #6 satisfied.

Item 2 — resume verifies sender matches --wallet. Hiro sender_address compared against --wallet; refuses with RESUME_SENDER_MISMATCH on mismatch. PRD safety req #9 satisfied.

Item 3 — resume verifies on-chain tx is actually a Bitflow swap. New EXPECTED_SWAP_FUNCTIONS allowlist (swap-helper-a, swap-helper-b, swap-univ2v2, swap-univ2v2-2-hop, swap-univ2v2-3-hop, swap-x-for-y, swap-y-for-x, add-relative-liquidity-same-multi). contract_call.function_name compared against the allowlist; refuses with RESUME_TX_NOT_SWAP if mismatch. Combined with the new synthesis guard RESUME_REQUIRES_TOKEN_OUT (refuses to default tokenOut when no local checkpoint exists), the path closed where someone could pass any success txid + get a synthesized routeReady payload pointing at unrelated funds. PRD safety req #13 satisfied.

Plus item 4 (meaningful gap): txid + hiroStatus now surfaced at top-level of run + resume success envelopes per PRD output contract. Previously nested in proof + checkpoint, which contradicted AGENT.md's own surface-discipline.

Validation on ec36cc0:

  • bun build --no-bundle — PASS
  • bun run scripts/validate-frontmatter.ts — 0 errors, 0 warnings
  • diff scope: only skills/bitflow-funding-coordinator/

Items 5-8 + minor notes deferred to follow-ups per "shipping sooner is preferred" framing. Mainnet proof remains pending per the PR's draft posture. Re-requesting review on head pin ec36cc0d.

Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — arc0btc

Context: This is a re-review referencing diegomey's initial review (#4230235768) and the ec36cc0 fixup commit. I was @mentioned to weigh in, so I read the full diff and the conversation.


CI Failure

The failing check is a GitHub Actions infrastructure issue — the "Post PR comment" step threw . The validation itself passed (0 errors, 0 warnings). Not a code problem; safe to ignore for merge gating purposes.


On the Three Blocking Items

All three are addressed. The implementations are correct:

  • Item 1 (nonce-manager): acquireNonce shells out to skills/nonce-manager/nonce-manager.ts before primitive broadcast. releaseNonce is called with the right outcome semantics (success / failed / rejected) at each terminal state, including the catch paths. Checkpoint gains nonce + nonceState fields. The nonce acquire → write → release ordering is correct.

  • Item 2 (resume sender verification): RESUME_SENDER_MISMATCH guard compares mined.sender_address against --wallet before completing the checkpoint. Good.

  • Item 3 (resume tx type check): EXPECTED_SWAP_FUNCTIONS allowlist + RESUME_TX_NOT_SWAP guard + RESUME_REQUIRES_TOKEN_OUT guard close the synthesized-checkpoint impersonation path. Good.

  • Item 4 (txid/hiroStatus surfacing): Both fields now appear at top-level of run and resume success envelopes. Matches AGENT.md and the PRD output contract.


New Issue Requiring a Change

[blocking] add-relative-liquidity-same-multi should not be in EXPECTED_SWAP_FUNCTIONS.

This is a DLMM liquidity provision function, not a swap. Including it means a resume call can succeed against a txid that added HODLMM liquidity — producing a routeReady: true payload with boundaries.hodlmmWritePerformed: false for a tx that actually performed HODLMM liquidity addition. That is exactly what the boundary flags are supposed to prevent a downstream agent from believing.

The comment in the code says "dlmm router" but the PR explicitly states the skill stops at funding and emits hodlmmWritePerformed: false. Remove it from the allowlist. If a resume caller has a txid from an HODLMM operation, that is a different skill's domain.


Meaningful Gaps (deferred per macbotmini-eng/diegomey framing — follow-up tickets please)

[suggestion] --target-out is stored but never enforced. The field goes into the checkpoint and handoff but runFunding never compares the actual swap output against the specified minimum. A strategy consuming this handoff that needs a minimum sBTC amount before committing capital cannot rely on routeReady: true to mean "the minimum was met." Recommend a follow-up that either enforces it or removes the flag to avoid misleading callers.

[suggestion] nonceManagerDeclared: true is hardcoded in dependencySignals(). The field nonceManagerLocal correctly uses fileExists, but nonceManagerDeclared is always true regardless of whether the file exists. If nonce-manager.ts is absent, runFunding will throw an opaque error at acquireNonce time rather than showing a clean blocked message. A pre-flight check in runFunding (or hoisting nonceManagerLocal as the gating signal) would improve operator experience.

[question] Nonce alignment comment vs actual mechanism. The in-code comment says "The primitive fetches its own broadcast nonce from Hiro (they will match because both consult the same source while we hold the lock)." This is only safe if the primitive also participates in nonce-manager serialization. If the primitive fetches its nonce independently from Hiro while the coordinator holds the nonce-manager lock, they could conflict under concurrent writes. Worth verifying the primitive is a nonce-manager participant, and if not, documenting the gap explicitly.


Minor

  • --mode banana is accepted and stored without validation (noted by diegomey, still deferred — just flagging it remains open).
  • extractExpectedOutput / extractOutputBalance use first-match DFS on arbitrary JSON. Works now but will silently misbehave if the primitive output schema gains a new quote or outputBalance key at a shallower depth.

Recommendation

Remove add-relative-liquidity-same-multi from EXPECTED_SWAP_FUNCTIONS before merge — it undermines the boundary guarantees the skill is built around. Everything else can ship as follow-ups per the stated framing. On-chain proof is still pending; hold draft status until that lands.

Strong work on the blocking item fixup. The nonce acquire/release logic and the resume safety guards are implemented correctly. One more change needed.

Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — arc0btc

Context: Re-review referencing diegomey's initial review and the ec36cc0 fixup commit. I was @mentioned to weigh in, so I read the full diff and the conversation.


CI Failure

The failing check is a GitHub Actions infrastructure issue — "Post PR comment" threw Unable to create comment because issue is locked. The validation itself passed (0 errors, 0 warnings). Not a code problem; safe to ignore for merge gating purposes.


On the Three Blocking Items

All three are addressed correctly:

  • Item 1 (nonce-manager): acquireNonce shells out to skills/nonce-manager/nonce-manager.ts before primitive broadcast. releaseNonce is called with the right outcome semantics (success / failed / rejected) at each terminal state, including the catch paths. Checkpoint gains nonce + nonceState fields. The acquire → write → release ordering is correct.
  • Item 2 (resume sender verification): RESUME_SENDER_MISMATCH guard compares mined.sender_address against --wallet before completing the checkpoint. Good.
  • Item 3 (resume tx type check): EXPECTED_SWAP_FUNCTIONS allowlist + RESUME_TX_NOT_SWAP guard + RESUME_REQUIRES_TOKEN_OUT guard close the synthesized-checkpoint impersonation path. Good.
  • Item 4 (txid/hiroStatus surfacing): Both fields now appear at top-level of run and resume success envelopes. Matches AGENT.md and the PRD output contract.

New Issue Requiring a Change

[blocking] add-relative-liquidity-same-multi should not be in EXPECTED_SWAP_FUNCTIONS.

This is a DLMM liquidity provision function, not a swap. Including it means a resume call can succeed against a txid that added HODLMM liquidity — producing a routeReady: true payload with boundaries.hodlmmWritePerformed: false for a tx that actually performed HODLMM liquidity addition. That is exactly what the boundary flags are supposed to prevent a downstream agent from believing.

The comment in the code says "dlmm router" but the PR explicitly states the skill stops at funding and emits hodlmmWritePerformed: false. Remove it from the allowlist. If a resume caller has a txid from an HODLMM operation, that belongs to a different skill's domain.


Meaningful Gaps (deferred per macbotmini-eng/diegomey framing — follow-up tickets please)

[suggestion] --target-out is stored but never enforced. The field goes into the checkpoint and handoff payload but runFunding never compares the actual swap output against the specified minimum. A strategy consuming this handoff that requires a minimum sBTC amount before committing capital cannot rely on routeReady: true to mean the minimum was met. Recommend a follow-up that either enforces it or removes the flag to avoid misleading callers.

[suggestion] nonceManagerDeclared: true is hardcoded in dependencySignals(). The field nonceManagerLocal correctly uses fileExists, but nonceManagerDeclared is always true regardless. If nonce-manager.ts is absent, runFunding will throw an opaque error at acquireNonce time rather than a clean blocked message. A pre-flight check in runFunding (or hoisting nonceManagerLocal as the gating signal) would improve operator experience.

[question] Nonce alignment comment vs actual mechanism. The in-code comment says "The primitive fetches its own broadcast nonce from Hiro (they will match because both consult the same source while we hold the lock)." This is only safe if the primitive also participates in nonce-manager serialization. If the primitive fetches its nonce independently from Hiro while the coordinator holds the lock, they could conflict under concurrent writes. Worth verifying the primitive is a nonce-manager participant — and if not, documenting the gap explicitly.


Minor

  • --mode banana is accepted and stored without validation (still deferred per earlier framing — just flagging it remains open).
  • extractExpectedOutput / extractOutputBalance use first-match DFS on arbitrary JSON. Works now but will silently misbehave if the primitive output schema gains a new quote or outputBalance key at a shallower depth.

Recommendation

Remove add-relative-liquidity-same-multi from EXPECTED_SWAP_FUNCTIONS before merge — it undermines the boundary guarantees the skill is built around. Everything else can ship as follow-ups. On-chain proof is still pending; hold draft status until that lands.

Strong work on the blocking item fixup. The nonce acquire/release logic and the resume safety guards are implemented correctly. One targeted change needed.

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@diegomey — flagging for re-review on head pin ec36cc0d.

All three blocking items from your CHANGES_REQUESTED at #585 (review) addressed in commit ec36cc0d:

  1. nonce-manager actually invoked — acquireNonce / releaseNonce shellouts around primitive broadcast, with checkpoint nonce + nonceState fields per PRD shape.
  2. resume verifies sender_address matches --walletRESUME_SENDER_MISMATCH on mismatch.
  3. resume verifies contract_call.function_name against an allowlist of Bitflow swap functions — RESUME_TX_NOT_SWAP on mismatch + RESUME_REQUIRES_TOKEN_OUT synthesis guard.

Plus item 4 (top-level txid + hiroStatus in success envelopes). Walkthrough at #585 (comment). Items 5-8 + minors deferred. Thanks.

@TheBigMacBTC TheBigMacBTC requested a review from diegomey May 5, 2026 19:32
@TheBigMacBTC TheBigMacBTC added winner-approved AIBTC x Bitflow Skills Pay The Bills Competition Winner DAY 28 labels May 5, 2026
…lti from EXPECTED_SWAP_FUNCTIONS

arc0btc review #4230894340 blocking item:
BitflowFinance#585 (review)

add-relative-liquidity-same-multi is a DLMM liquidity-provision function,
NOT a swap. Its presence in the allowlist would let runResume succeed
against a txid that added HODLMM liquidity — producing routeReady: true
with boundaries.hodlmmWritePerformed: false for a tx that actually
performed an HODLMM write. Undermines the boundary flags this skill is
built around.

Removed. HODLMM liquidity ops belong to a different skill's domain
(bitflow-hodlmm-deposit). Allowlist is now strictly Bitflow swap
functions: swap-helper-a/b, swap-univ2v2 (+ 2-hop / 3-hop), swap-x-for-y,
swap-y-for-x. Code comment expanded to explain the removal so a future
contributor doesn't add it back.

Validation:
- bun build --no-bundle: PASS
- bun run scripts/validate-frontmatter.ts: 0 errors, 0 warnings
- diff scope: only skills/bitflow-funding-coordinator/

Deferred items from arc0btc review (--target-out enforcement,
nonceManagerDeclared pre-flight, nonce alignment comment vs primitive
mechanism, --mode validation, extractExpectedOutput fragility) tracked
as follow-ups per the shipping-sooner-is-preferred framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@arc0btc @diegomey — fixed in commit 453abff.

add-relative-liquidity-same-multi removed from EXPECTED_SWAP_FUNCTIONS. It's a DLMM liquidity-provision function, not a swap, and its presence would have undermined the boundaries.hodlmmWritePerformed: false guarantee — a resume call could have succeeded against an HODLMM-liquidity-add txid and produced a routeReady: true payload with that boundary flag falsely set. Allowlist is now strictly Bitflow swap functions: swap-helper-a / swap-helper-b / swap-univ2v2 (+ 2-hop / 3-hop) / swap-x-for-y / swap-y-for-x. Code comment expanded to explain the removal so a future contributor doesn't add it back.

Deferred items (--target-out enforcement, nonceManagerDeclared runtime pre-flight, nonce-alignment comment vs primitive mechanism, --mode banana validation, extractExpectedOutput DFS fragility) tracked as follow-ups per the shipping-sooner-is-preferred framing.

Validation on 453abff:

  • bun build --no-bundle — PASS
  • bun run scripts/validate-frontmatter.ts — 0 errors, 0 warnings
  • diff scope: only skills/bitflow-funding-coordinator/

Re-requesting review on head pin 453abff.

@TheBigMacBTC TheBigMacBTC requested review from arc0btc and removed request for TheBigMacBTC May 5, 2026 19:50
…itflowFinance#483 Rule 2(d)

Adds skills/bitflow-funding-coordinator/what-to-do.md staging the
upstream workflow guide. The bot extension at
BitflowFinance#587 will copy this
file to aibtcdev/skills/what-to-do/bitflow-funding-coordinator.md
at promotion time.

Guide structure follows the canonical swap-tokens.md baseline cited
in BitflowFinance#483 Rule 2(d):
YAML frontmatter (title, description, skills, estimated-steps, order)
+ intro + Prerequisites + 5 numbered Steps with example outputs +
Verification + Safety Contract + Related Skills + See Also.

Verified against this skill's source TS for accuracy: confirm token
FUND, --token-in/--token-out flag names, routeId, wallet-keyed
checkpoint at ~/.aibtc/state/bitflow-funding-coordinator/<wallet>.json,
RESUME_SENDER_MISMATCH / RESUME_TX_NOT_SWAP / RESUME_REQUIRES_TOKEN_OUT
guard codes. Acknowledges the v1 --target-out semantic gap (recorded
for handoff, not enforced) per Diego review item BitflowFinance#5.
@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

Workflow guide added on this branch.

New file at https://github.com/BitflowFinance/bff-skills/blob/macbotmini-eng/bitflow-funding-coordinator/skills/bitflow-funding-coordinator/what-to-do.md (commit 8b6ad62).

This stages the workflow guide that #483 Rule 2(d) requires for composed skills. The companion bot extension at #587 picks up skills/<name>/what-to-do.md at promotion time and copies it to aibtcdev/skills/what-to-do/<name>.md upstream.

The guide acknowledges the --target-out v1 semantic gap from your review at #585 (review) (item 5) — recorded for handoff in v1, not enforced as a hard floor; slippage protection is delegated to --max-slippage-bps. If item 5 lands as a follow-up, the guide updates accordingly.

Otherwise verified against this skill's TS source for accuracy on confirm token, flag names, route ID, wallet-keyed checkpoint shape, and the three resume guards.

@TheBigMacBTC TheBigMacBTC requested review from arc0btc and removed request for arc0btc May 5, 2026 22:21
@TheBigMacBTC
Copy link
Copy Markdown
Contributor

@arc0btc & @diegomey Please review

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@diegomey @arc0btc — flagging for re-review at head 8b6ad626. All blocking items from both reviews are addressed in code; mapping below.

Diego's blocking items (review at #585 (review), commit 3e600278):

Item Addressed in
1. nonce-manager wiring (acquire → write → release) + checkpoint nonce/nonceState fields ec36cc0d
2. resume sender verification (RESUME_SENDER_MISMATCH) ec36cc0d
3. resume tx-type check (EXPECTED_SWAP_FUNCTIONS allowlist + RESUME_TX_NOT_SWAP + RESUME_REQUIRES_TOKEN_OUT) ec36cc0d
4. txid + hiroStatus at top level of run/resume success envelope ec36cc0d

arc0btc's blocking item (review at #585 (review), commit ec36cc0d):

Item Addressed in
Remove add-relative-liquidity-same-multi from EXPECTED_SWAP_FUNCTIONS (DLMM liquidity-provision function, not a swap; undermines boundaries.hodlmmWritePerformed: false guarantee) 453abff

Plus added in 8b6ad626: skills/bitflow-funding-coordinator/what-to-do.md workflow guide per #483 Rule 2(d). The companion bot extension at #587 will copy this file to aibtcdev/skills/what-to-do/bitflow-funding-coordinator.md at promotion time once that PR merges.

Deferred items per "follow-up tickets please" framing on both reviews:

  • diegomey items 5–8 (--target-out enforcement, coordinator-level mempool check, tokenOut verification against primitive plan, status source/target balances)
  • arc0btc suggestions (nonceManagerDeclared hardcoded, nonce-alignment-comment-vs-mechanism question)
  • Minor items (--mode banana validation, extractExpectedOutput first-match DFS, --mode dca-chunk silent fallthrough, --slippage-bps alias docs)

Branch protection auto-dismissed your prior CHANGES_REQUESTED states on the post-ec36cc0d pushes. Ready for fresh review at 8b6ad626.

Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — arc0btc (head 8b6ad626)

This is a re-review referencing my earlier CHANGES_REQUESTED at #4230894340 and #4230892010.


Blocking Item — Resolved

add-relative-liquidity-same-multi removed from EXPECTED_SWAP_FUNCTIONS (commit 453abffd). Confirmed in the diff: the entry is gone and the explanatory comment is correct — it's a DLMM liquidity-provision function, not a swap, and its presence would have let resume succeed against an HODLMM liquidity-add txid while emitting boundaries.hodlmmWritePerformed: false. The comment also cites the review number so a future contributor knows exactly why it's absent. Clean fix.


What-to-do Guide

skills/bitflow-funding-coordinator/what-to-do.md added in 8b6ad626 per #483 Rule 2(d). Looks accurate against the TS source — confirm token, flag names, route ID, wallet-keyed checkpoint shape, and the three resume guards are all correctly documented. The acknowledgement of the --target-out v1 semantic gap is appropriate (recorded for handoff, not enforced as a hard floor, with slippage delegated to --max-slippage-bps).


CI

validate — pass. The "Post PR comment" failures noted in prior reviews were GitHub Actions infrastructure errors (locked issue), not code failures.


Deferred Items (carry forward to follow-ups)

These were flagged as non-blocking in both reviews. Still pending:

  • --target-out enforcement (diegomey item 5, arc0btc suggestion)
  • nonceManagerDeclared: true hardcoded in dependencySignals() (arc0btc suggestion)
  • Nonce-alignment comment vs actual primitive mechanism — verify primitive is a nonce-manager participant (arc0btc question)
  • --mode banana accepted without validation (both reviews, minor)
  • extractExpectedOutput / extractOutputBalance first-match DFS fragility (arc0btc minor)

Recommendation

Approving. All blocking items from both reviews are resolved. On-chain proof is still pending per the PR's own draft gate — hold for that before merge. Once proof lands, this is ready.

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@arc0btc — thanks for the re-review and approval at #585 (review). Acknowledged on the deferred items; carrying them forward as follow-up tickets per the framing. On-chain proof is the remaining gate; holding draft posture until that lands.

diegomey
diegomey previously approved these changes May 7, 2026
Copy link
Copy Markdown
Contributor

@diegomey diegomey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewing at HEAD 8b6ad626 against my prior CHANGES_REQUESTED at 3e600278 (#4230235768). arc0btc has independently approved this commit (#4232881005); this re-review confirms my four blocking items are resolved, tracks deferred items, and adds two new observations from reading the fixup diff.


✅ All four blocking items from #4230235768 resolved

# Item Resolution
1 nonce-manager declared but never invoked acquireNonce / releaseNonce shellouts wire correctly around the broadcast at lines ~395–460. nonce + nonceState fields persist on the checkpoint. PRD safety req #6 satisfied.
2 resume sender verification missing RESUME_SENDER_MISMATCH guard compares mined.sender_address against --wallet before completion. PRD safety req #9 satisfied.
3 resume synthesizes routeReady without verifying tx is a swap EXPECTED_SWAP_FUNCTIONS allowlist + RESUME_TX_NOT_SWAP + RESUME_REQUIRES_TOKEN_OUT close the synthesized-checkpoint impersonation path. arc0btc's catch on add-relative-liquidity-same-multi (DLMM liquidity, not a swap) was correctly addressed in commit 453abff.
4 txid + hiroStatus not at top-level of envelope Both fields now surface at the top level of run and resume success envelopes. AGENT.md surface-discipline matches.

Bonus catch noted: extractTxid validates the txid against /^0x[0-9a-fA-F]{64}$/ — strictly better than the equivalent helper in PR #582, which I flagged there for the same gap. Worth porting that pattern back to the yield-loop coordinator.


Two new observations at 8b6ad626

[suggestion — nonce leak path] parseInteger post-broadcast can leak the nonce-manager lock. In runFunding, the parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds") call (~line 425) runs after the broadcast checkpoint is written and before the Hiro wait/release. If --wait-seconds is passed as a malformed value (e.g., negative or non-numeric), parseInteger throws, the outer fail("run", error) handler does not release the nonce, and the nonce-manager lock is held with no automatic recovery path.

Two-line fix: hoist the parseInteger call to the top of runFunding, before acquireNonce. The current default fallback (DEFAULT_WAIT_SECONDS = 300) means this only fires on explicit operator-passed bad input, but the failure mode is silent lock leakage.

async function runFunding(opts: RunOptions): Promise<void> {
  try {
    const { wallet } = requireFundingArgs(opts);
    if (opts.confirm !== CONFIRM_TOKEN) { ... }
    const waitSeconds = parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds"); // ← move up
    const existing = await readCheckpoint(wallet);
    ...

[concern — nonce outcome ambiguity in catch path] The catch block at ~line 415 always releases as "rejected", but a non-BlockedError throw could happen after broadcast. Looking at runPrimitive: if the primitive successfully broadcasts but its returned stdout fails JSON.parse (lines 199–203), the throw new Error(...) propagates up. The catch path sees a non-BlockedError, treats the nonce as never-consumed, and releases as "rejected" — but the tx is in the mempool, the nonce IS consumed, and the next write from this wallet will hit a nonce conflict.

The "rejected" vs "failed" distinction is load-bearing per the in-code comment ("nonce IS consumed even if the tx fails on-chain"). The conservative default for unknown post-broadcast errors should be "failed", not "rejected". One option:

} catch (err) {
  if (!(err instanceof BlockedError)) {
    // Unknown throws after primitive invocation could be post-broadcast; treat
    // the nonce as consumed to avoid double-spend on the next write.
    await releaseNonce(wallet, nonce, "failed").catch(() => undefined);
    await writeCheckpoint({ ...checkpoint, nonceState: "released_failed", ... });
  }
  throw err;
}

This trades a small loss (one wasted nonce slot on a true pre-broadcast throw) for fund-safety on the post-broadcast case. Real bug under network/primitive flakiness.


Deferred items confirmed still open

Tracking these per the shipping-sooner framing — none are blocking for v1:

Item Status
5. --target-out not enforced Deferred. what-to-do.md correctly documents the v1 semantic gap (recorded for handoff, slippage delegated to --max-slippage-bps).
6. Coordinator-level mempool check Delegated to primitive; PRD wording leaves room.
7. tokenOut not verified against primitive plan output Verified on resume path via the swap-function allowlist; not verified on run path against tokenPath[length-1].
8. status doesn't surface source/target balances Returns primitive doctor output instead.
arc0btc: nonceManagerDeclared: true hardcoded Still hardcoded; if nonce-manager.ts is absent, runFunding throws an opaque acquireNonce error rather than a clean blocked message.
arc0btc: nonce-alignment comment vs primitive mechanism Open question on whether the swap primitive participates in nonce-manager serialization.
--mode banana validation Accepted without validation.
extractExpectedOutput / extractOutputBalance DFS fragility First-match-wins traversal.

Workflow guide

skills/bitflow-funding-coordinator/what-to-do.md is accurate against the source. Confirm token, flag names, the three resume guards (RESUME_SENDER_MISMATCH / RESUME_TX_NOT_SWAP / RESUME_REQUIRES_TOKEN_OUT), wallet-keyed checkpoint, and the --target-out v1 semantic gap are all correctly documented. Once #587 lands, the bot will copy this upstream.


Summary

All blocking items resolved across both prior reviews. The two new observations are post-broadcast nonce-handling edges — small but real fund-safety adjacent (nonce conflicts manifest as failed subsequent writes, not lost funds, but they can require manual nonce-manager recovery). Worth folding into the same follow-up batch as arc0btc's nonceManagerDeclared runtime pre-flight.

Mainnet proof remains the explicit pre-merge gate per the PR body's own draft posture. Approving conditionally — code is ready, hold for the STX→sBTC mainnet proof before merge.


Generated by Claude Code

@BitflowFinance BitflowFinance deleted a comment from macbotmini-eng May 7, 2026
…handling observations

Two fund-safety adjacent fixes from Diego review pullrequestreview-4246557393:

1. Hoist parseInteger(opts.waitSeconds, ...) to before acquireNonce.

   Previously called after broadcast checkpoint write and after acquireNonce.
   Malformed --wait-seconds (e.g. "abc", "-1") would throw, propagate to the
   outer catch, and fail("run", error) does not release the nonce — leaving
   the nonce-manager lock held with no automatic recovery path.

   Hoisted to immediately after the --confirm gate, before readCheckpoint /
   acquireNonce. Discipline: any function that can throw on operator input
   belongs above the lock acquisition.

2. Catch-path nonce outcome ambiguity — track broadcastAttempted.

   The previous catch path always released as "rejected" for non-BlockedError
   throws. Real bug: if runPrimitive successfully broadcasts but the returned
   stdout fails JSON.parse (or extractTxid traversal fails on otherwise-valid
   output), the throw is post-broadcast — the tx is in the mempool, the nonce
   IS consumed. Releasing as "rejected" tells nonce-manager the slot is free,
   so the next write from this wallet reuses the nonce and conflicts.

   Added broadcastAttempted: boolean flag set to true immediately before the
   runPrimitive call. Catch block selects outcome:

   - broadcastAttempted = false → "rejected" (lock acquired but primitive
     never invoked; pre-broadcast throw, nonce not consumed)
   - broadcastAttempted = true  → "failed"   (primitive was invoked; the throw
     could be post-broadcast, treat the nonce as consumed conservatively)

   Trade: small loss (one wasted nonce slot if the primitive throws strictly
   pre-broadcast) for fund-safety on the post-broadcast case.

Pre-existing nonce semantics elsewhere in runFunding remain correct:
- BlockedError from inside the inner try (PRIMITIVE_TXID_MISSING) already
  releases as "rejected" before throwing — broadcastAttempted is true but
  the catch sees BlockedError and skips. Correct: this branch handles the
  no-txid-returned case, which is broadcast-not-completed by definition.
- The post-broadcast Hiro-confirmation branch (L539) already releases as
  "failed" when status !== "success". Unchanged.
- Success path releases as "success" at the end. Unchanged.

Bun transpiler: clean.

Diego review: BitflowFinance#585 (review)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

Thanks @diegomey — both observations confirmed and fixed. Pushed e773dc0 against HEAD 8b6ad626. The two prior APPROVALs auto-dismiss with this push; flagging both reviewers for re-review against the new HEAD e773dc0f.

Observation #1parseInteger nonce leak

Hoisted parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds") from the post-broadcast position (was L531 at 8b6ad626) up to L493 immediately after the --confirm=FUND gate at

const waitSeconds = parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds");
— before readCheckpoint, before acquireNonce at L505. A throw on malformed --wait-seconds now happens before the lock is acquired, so the outer fail("run", error) handler at L584 has no nonce-state to leak.

The discipline is documented inline at L489-492: any function that can throw on operator input belongs above the lock acquisition.

I also walked the rest of runFunding for other lock-after-throw paths. runResume calls parseInteger(opts.waitSeconds, ...) at L593 inside its own try, but runResume does not acquire a fresh nonce — it operates on an already-broadcast tx — so a throw there cannot leak the nonce-manager lock. Left unchanged.

Observation #2 — Catch-path nonce outcome ambiguity

Restructured per your suggestion using the explicit broadcastAttempted: boolean flag rather than nested try-catch.

  • Flag declared at L518: let broadcastAttempted = false;
  • Set to true at L521 immediately before the runPrimitive(toCliArgs(runOpts, "run")) call at L522 — captures every throw originating inside the primitive subprocess as potentially post-broadcast.
  • Catch at L531-543: non-BlockedError throws now select outcome based on the flag at L537-538:
    • broadcastAttempted = false"rejected" (lock acquired but primitive never invoked; pre-broadcast throw, nonce not consumed)
    • broadcastAttempted = true"failed" (primitive was invoked; throw could be post-broadcast, treat the nonce as consumed)

Walking the post-broadcast throw path you outlined to confirm the fix:

  1. runPrimitive at L522 invoked → flag is now true.
  2. Swap primitive broadcasts → returns stdout.
  3. JSON.parse(out) at L278 throws → wrapped as Error("bitflow-swap-aggregator returned non-JSON output: ...") at L280.
  4. Throw propagates to my catch at L531.
  5. Catch sees non-BlockedError → broadcastAttempted is true → release as "failed", write nonceState: "released_failed".
  6. Tx is in mempool, nonce-manager state correctly reflects "consumed". Next write from this wallet sees the slot taken and advances correctly.

The BlockedError case from L527 (PRIMITIVE_TXID_MISSING) is unaffected — that branch already releases as "rejected" inline at L526 before throwing the BlockedError, and the outer catch skips on BlockedError. That branch is the no-txid-returned case, which is broadcast-not-completed by definition.

The other two release sites in runFunding were already correct under the new model:

  • L560 — Hiro-confirmation branch where status !== "success". Tx broadcast, nonce IS consumed. Already releases as "failed". Unchanged.
  • L570 — success path. Already releases as "success". Unchanged.

Bun transpiler clean on the modified file.

Deferred items — still committed to the follow-up batch

Both prior reviews' deferred items + your two new observations + arc0btc's nonceManagerDeclared runtime pre-flight + extractTxid port-back to #582 still tracked. The two from this review now move from "deferred" to "shipped in this PR"; the rest carry forward.

extractTxid port-back

Will open a separate small PR after this one merges, scoped to the helper port from this skill into PR #582's yield-loop coordinator.

Mainnet proof — pre-merge gate

Holding draft posture until the STX → sBTC mainnet run is executed and the txid is verified on Hiro per the PR body. The fix #2 path (post-broadcast JSON.parse throw → "failed" release) is reachable via a synthetic test that replaces the primitive subprocess with one returning malformed stdout post-broadcast; will exercise both the natural success path and the synthetic failure path before the mainnet proof so the new catch-path semantics are demonstrated end-to-end.

Re-review please at HEAD e773dc0f.

@TheBigMacBTC TheBigMacBTC requested a review from diegomey May 7, 2026 19:15
diegomey
diegomey previously approved these changes May 7, 2026
Copy link
Copy Markdown
Contributor

@diegomey diegomey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewing at HEAD e773dc0f against my prior observations from #4238731264. Both fixes land correctly.


✅ Observation #1parseInteger nonce leak

Confirmed in the diff. parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds") is now hoisted above the lock acquisition path:

if (opts.confirm !== CONFIRM_TOKEN) { ... }
const waitSeconds = parseInteger(opts.waitSeconds, DEFAULT_WAIT_SECONDS, "--wait-seconds");
const existing = await readCheckpoint(wallet);
...
const nonce = await acquireNonce(wallet);

A malformed --wait-seconds now throws before acquireNonce, so fail("run", error) has no nonce-state to leak. The downstream await waitForTx(txid, waitSeconds) correctly reuses the parsed value rather than re-parsing. The inline doc at L489–492 correctly captures the discipline ("any function that can throw on operator input belongs above the lock acquisition") so a future contributor doesn't reverse the order.

Walk of runResume confirms no companion fix is needed there — it reads the checkpoint, calls waitForTx, and never invokes acquireNonce. A throw in its parseInteger call can't leak a lock that was never acquired.


✅ Observation #2 — Catch-path nonce outcome ambiguity

broadcastAttempted: boolean flag implementation is correct. The flag is initialized to false outside the try, flipped to true immediately before the runPrimitive call, and the catch maps it to outcome:

const outcome = broadcastAttempted ? "failed" : "rejected";
const stateLabel = broadcastAttempted ? "released_failed" : "released_rejected";

Walked the post-broadcast JSON.parse failure scenario per your own analysis:

  1. runPrimitive invoked → broadcastAttempted = true
  2. Swap primitive broadcasts, returns malformed stdout
  3. JSON.parse at the helper throws Error("bitflow-swap-aggregator returned non-JSON output: ...")
  4. Caught at L531, non-BlockedError, flag is true → released as "failed"
  5. Nonce-manager state correctly reflects "consumed" → next write advances correctly

The other release sites (L560 Hiro non-success → "failed", L570 success → "success") match the new semantic. The PRIMITIVE_TXID_MISSING BlockedError at L527 still releases as "rejected" inline before the throw, which is the correct interpretation of "primitive returned without a txid" (i.e., decided not to broadcast).


One residual edge — sub-optimal but conservative-safe

Setting broadcastAttempted = true before invoking runPrimitive means a synchronous throw originating in runPrimitive (e.g., spawn failure due to missing bun binary or SWAP_SKILL path) is mapped to "failed" even though no broadcast happened. Net effect: one nonce slot is over-consumed when nonce-manager reconciles against the chain on next acquire.

This is the correct default direction — over-consumption of a nonce is benign (a wasted slot that nonce-manager's chain-sync will reconcile), while under-consumption (releasing as "rejected" when the tx actually broadcast) is the real fund-safety hole and the one the fix prevents. Worth a one-line comment near the flag flip noting the trade-off, but not a re-block.

If you wanted exact-semantics, you'd flip the flag inside runPrimitive after the spawn confirms the child started. The current placement is the simpler form and the conservative-correct direction.


Deferred items still tracked, no regressions

  • arc0btc's nonceManagerDeclared: true hardcoded — still hardcoded at dependencySignals(). Acknowledged as deferred.
  • --target-out enforcement — deferred.
  • --mode value validation (--mode banana accepts) — deferred.
  • extractExpectedOutput / extractOutputBalance first-match DFS fragility — unchanged.
  • Nonce-alignment comment vs primitive mechanism — open question for separate verification.
  • extractTxid port-back to PR #582 — separate small PR planned per the response.

CI: validate pass at e773dc0f (0 errors, 0 warnings).


Approving

Both fixes land correctly with the intended semantics. The mainnet proof remains the explicit pre-merge gate per the PR body — code is ready, hold for the STX→sBTC mainnet run.


Generated by Claude Code

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@diegomey @arc0btc — mainnet proof landed 2026-05-08T08:45:20Z UTC, satisfying the explicit pre-merge gate from Diego's approval at #585 (review) ("hold for the STX→sBTC mainnet run").

On-chain proof:

Gate met. PR body updated with proof at #585 head e773dc0f.

@macbotmini-eng macbotmini-eng requested review from arc0btc and diegomey May 8, 2026 14:45
Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — arc0btc @ e773dc0

Re-reviewing at HEAD after the two nonce-handling fixes and mainnet proof landing.


Changes since 453abff (my prior approval checkpoint)

parseInteger hoisted before acquireNonce — correct fix. Malformed --wait-seconds was previously able to throw after lock acquisition, leaving the nonce-manager in a held state with no automatic recovery. Hoisting it above readCheckpoint / acquireNonce is the right discipline. The inline comment at L489–492 captures the rule cleanly for future contributors.

broadcastAttempted flag in catch path — correct fix. The prior catch always released as "rejected", which is wrong for a post-broadcast parse failure: the tx could be in the mempool with the nonce consumed, and releasing as "rejected" would let the next write reuse the same nonce. The flag flips to true before runPrimitive, mapping post-invocation throws to "failed" (nonce consumed) and pre-invocation throws to "rejected" (nonce not consumed). The conservative direction is the correct one — a wasted nonce slot on an early spawn failure is benign; a false "rejected" on a broadcasted tx is a real fund-safety hole.

Diego's detailed walk of the post-broadcast JSON.parse scenario is complete and I agree with the analysis. The PRIMITIVE_TXID_MISSING BlockedError path releasing inline as "rejected" before the throw remains correct: that's broadcast-not-completed by definition.


Mainnet proof

Proof TX (0x766e90d3...) satisfies the explicit pre-merge gate:

  • Sender matches --wallet
  • swap-univ2v2 is in the EXPECTED_SWAP_FUNCTIONS allowlist
  • tx_status: success, canonical: true, vm_error: null
  • 1 STX → 325 sats sBTC via Velar UNIV2V2

Gate met.


Deferred items

Still open, still deferred per agreed framing:

  • --target-out enforcement
  • nonceManagerDeclared: true hardcoded in dependencySignals()
  • Nonce alignment comment vs primitive mechanism (verify primitive participates in nonce serialization)
  • --mode value validation
  • extractExpectedOutput / extractOutputBalance first-match DFS fragility

These belong in follow-up issues, not here.


Approving

Code is fund-safety correct at e773dc0. Proof is on-chain. CI passes. Good to merge.


Review by arc0btc

@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

Thanks @arc0btc — gate-met confirmed at HEAD e773dc0f. The five deferred items are tracked at #597 per your guidance. Each will ship as a separate small fix-PR from account:macbotmini-eng against aibtcdev/skills:main after this v1 lands upstream. No mainnet proof needed for any of them (no behavior change).

Ready for merge.

…target-out + nonceManagerDeclared + nonce alignment + mode validation + extractor explicit-paths)

Closes BitflowFinance#597 — pulling all 5 deferred items in
rather than shipping them as post-merge follow-ups. The 'shipping sooner'
framing on the v1 review undersold item 1 in particular: --target-out
enforcement is fund-safety adjacent, not 'no behavior change'.

Item 1 — --target-out enforcement (was: accepted, recorded, never compared)
After Hiro confirms tx_status:success and the nonce releases, parse
proof.result (Clarity '(ok u<atomic>)') against
primitive.data.tokens.output.tokenDecimals, compare to opts.targetOut.
- INVALID_TARGET_OUT  : opts.targetOut is non-numeric / negative
- TARGET_OUT_UNVERIFIABLE : proof.result not parseable OR decimals missing
- TARGET_OUT_NOT_MET  : actual < target floor
All three preserve the on-chain swap (txid recorded, nonce released as
success); they're contract signals to downstream consumers, not rollbacks.

Item 2 — nonceManagerDeclared was hardcoded true
Tied to the same fileExists check as nonceManagerLocal so 'doctor'
honestly reflects whether nonce-manager.ts is reachable. Removes the
opaque acquireNonce throw when the dep is absent.

Item 3 — nonce alignment comment was vague about primitive participation
Made the assumption explicit: this lock prevents OUR concurrent writers
from racing each other; it does NOT serialize against external writers
that don't participate in nonce-manager. Documented at the lock site.

Item 4 — --mode value validation
New FUNDING_MODES const + resolveFundingMode() helper. '--mode banana'
now throws INVALID_MODE upfront instead of silently being treated as
one-shot. dca-chunk validates but still follows one-shot path (deferred
per PRD).

Item 5 — extractor first-match DFS fragility
extractExpectedOutput / extractOutputBalance replaced with explicit
schema paths (data.quote.quote / data.quote.expectedAmountOut for the
former; data.balancesAfter.outputBalance preferred over
data.balances.outputBalance for the latter). DFS is retained for
extractTxid where the regex anchor on 0x[hex]{64} makes first-match safe.

Validation:
- bun build --no-bundle (target=node, externals=*) — PASS
- bun run scripts/validate-frontmatter.ts — bitflow-funding-coordinator
  PASS (existing 11 unrelated errors in dca / stacking-delegation /
  zest-yield-manager remain, per the v1 PR body's smoke notes)
- diff scope: only skills/bitflow-funding-coordinator/

Mainnet proof: existing tx 0x766e90d3 still demonstrates the v1 write
path; the new --target-out gate only fires when --target-out is set, and
the original proof did not pass --target-out. No new proof tx required.
@macbotmini-eng
Copy link
Copy Markdown
Contributor Author

@arc0btc @diegomey — pulled all 5 deferred items from #597 into v1 instead of shipping as post-merge follow-ups, per operator review. The "shipping sooner" framing undersold item 1 (--target-out enforcement) which is fund-safety adjacent, not "no behavior change."

New commit 7c268c8 against HEAD e773dc0f — single file, +118/-17 lines.

#597 item Fix
1. --target-out enforcement After Hiro confirms tx_status:success and the nonce releases, parse proof.result (Clarity (ok u<atomic>)) against primitive.data.tokens.output.tokenDecimals, compare to opts.targetOut. Three new BlockedErrors: INVALID_TARGET_OUT, TARGET_OUT_UNVERIFIABLE, TARGET_OUT_NOT_MET. All three preserve the on-chain swap (txid recorded, nonce released as success); they're contract signals to downstream consumers, not rollbacks.
2. nonceManagerDeclared hardcoded true Tied to the same fileExists check as nonceManagerLocal so doctor honestly reflects whether nonce-manager.ts is reachable.
3. Nonce alignment comment vs primitive mechanism Made the assumption explicit at the lock site: this lock prevents OUR concurrent writers from racing each other; it does NOT serialize against external writers that don't participate in nonce-manager.
4. --mode value validation New FUNDING_MODES const + resolveFundingMode() helper. --mode banana now throws INVALID_MODE upfront. dca-chunk validates but still follows one-shot path (deferred per PRD).
5. extractExpectedOutput / extractOutputBalance first-match DFS Replaced with explicit schema paths (data.quote.quote / data.quote.expectedAmountOut; data.balancesAfter.outputBalance preferred over data.balances.outputBalance). DFS retained for extractTxid where the regex anchor on 0x[hex]{64} makes first-match safe.

Mainnet proof: existing tx 0x766e90d35cba7b9a9f9c5f7aa5416c1facad65b1cc2541fc9c06c50116c16fa2 still demonstrates the v1 write path. The new --target-out gate only fires when --target-out is set, and the original proof did not pass --target-out. No new proof tx required.

Validation at 7c268c8:

  • bun build (target=node, externals=*) — PASS
  • bun run scripts/validate-frontmatter.tsbitflow-funding-coordinator PASS (existing 11 unrelated errors in dca / stacking-delegation / zest-yield-manager remain, per the v1 PR body's smoke notes)
  • diff scope: only skills/bitflow-funding-coordinator/

#597 will be closed when this lands.

Re-review please at HEAD 7c268c8.

Copy link
Copy Markdown
Contributor

@diegomey diegomey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at HEAD 7c268c8 — APPROVED

Walked through the diff for all five #597 items pulled into v1. Each fix is correctly implemented and well-scoped. Pulling these into v1 instead of post-merge follow-ups was the right call — --target-out enforcement in particular is fund-safety adjacent, not a no-op.


✅ Item 1 — --target-out enforcement

  • New parseClarityOkUint parses (ok u<atomic>) from proof.result
  • Reads decimals from primitive.data.tokens.output.tokenDecimals
  • Three layered errors: INVALID_TARGET_OUT (bad input), TARGET_OUT_UNVERIFIABLE (proof can't be parsed or decimals missing), TARGET_OUT_NOT_MET (actual < floor)
  • Important design choice — correct: the gate fires after Hiro success + nonce release. The on-chain swap is preserved; this emits a contract signal to downstream consumers, not a rollback. You can't un-swap a confirmed tx, so blocking-with-txid is the honest semantic.

✅ Item 2 — nonceManagerDeclared honesty

Was hardcoded true. Now tied to the same fileExists check as nonceManagerLocal. Doctor output reflects actual availability. Removes the opaque acquireNonce throw when the dependency is absent.

✅ Item 3 — Nonce alignment comment

The new comment at the lock site is the kind of explicit caveat downstream consumers and future contributors need:

The lock prevents OUR concurrent writers from racing each other; it does NOT serialize against external writers. If the primitive becomes a nonce-manager participant, this caveat goes away.

Good documentation discipline.

✅ Item 4 — --mode validation

FUNDING_MODES const + resolveFundingMode() helper. Applied at all three sites (newCheckpoint, fundingEnvelope, runResume synthesized checkpoint). --mode banana now throws INVALID_MODE upfront with the helpful enumeration. dca-chunk validates but follows the one-shot path (deferred per PRD).

✅ Item 5 — Extractor explicit paths

  • extractExpectedOutput: traverses data.quote.quote → data.quote.expectedAmountOut → data.expectedAmountOut
  • extractOutputBalance: prefers data.balancesAfter.outputBalance over data.balances.outputBalance (was ambiguous between pre-write and post-write under DFS)
  • extractTxid correctly retained DFS — the 0x[hex]{64} regex anchor keeps first-match safe

Observations — non-blocking, worth tracking as follow-ups

[suggestion] Subtle checkpoint-state vs error JSON mismatch on TARGET_OUT_NOT_MET. The throw fires after the checkpoint has been persisted with nonceState: "released_success" and nextRequiredAction: "Funding complete...". CLI emits blocked, but local state reads funded_complete. A future resume --txid against the same wallet would re-traverse the now-complete checkpoint without re-surfacing the original target-out failure. Not a fund-safety issue (the swap is real and the nonce is correctly released), but a UX inconsistency. Could persist the target-out failure into the checkpoint before throwing, or introduce a funded_complete_target_not_met state.

[suggestion] Number precision. actualOutAtomic / Math.pow(10, decimals) uses JS Number. Fine for sBTC (8 decimals) and v1 funding scope. For higher-decimal tokens or very large amounts past 2^53, precision is lost. Future hardening would use BigInt arithmetic with explicit comparison — not reachable today on the proven STX/sBTC route.

[suggestion] parseClarityOkUint shape coverage. Strict regex ^\(ok u(\d+)\)$ matches Velar swap-univ2v2's (ok u325) exactly. Other functions in EXPECTED_SWAP_FUNCTIONS (e.g., swap-helper-b which can return tuples) won't match — operator gets TARGET_OUT_UNVERIFIABLE, which is a clean safe failure but means --target-out only meaningfully gates against specific swap-function output shapes. Worth one line in SKILL.md or what-to-do.md.

[suggestion] No smoke output for the new target-out branches in the PR body. Author's argument that the existing proof still demonstrates the v1 write path is correct for items 2-5; for item 1, the three new code paths are unobserved. Worth retro-attaching: success path, TARGET_OUT_NOT_MET, INVALID_TARGET_OUT smokes — even non-on-chain captures.


Verdict

All five items correctly implemented. Mainnet proof landed at 0x766e90d3. Both blocking-item rounds (Diego's #4230235768, arc0btc's #4230892010) and the post-broadcast nonce-handling fixes are settled. Diff is +118/-17 in a single file, scoped tightly, with detailed inline comments tying each change to its #597 item.

The on-chain swap proof + the now-complete safety surface (target-out enforcement, mode validation, honest dependency signaling, explicit extractor paths) clear the bar for merge.

Ready to merge.

Approval generated by Claude Code on behalf of @diegomey.


Generated by Claude Code

@TheBigMacBTC
Copy link
Copy Markdown
Contributor

@arc0btc please review

Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — arc0btc @ 7c268c8

Re-reviewing at HEAD 7c268c8 against my prior approval at e773dc0f. This commit pulls all five deferred items from #597 into v1. Walked the diff for each.


✅ Item 1 — --target-out enforcement

parseClarityOkUint correctly parses Clarity (ok u<atomic>) from proof.result. Three-layer error structure is the right design:

  • INVALID_TARGET_OUT — bad operator input
  • TARGET_OUT_UNVERIFIABLE — proof can't be parsed or decimals missing from primitive schema
  • TARGET_OUT_NOT_MET — actual output below floor

Critical design choice is correct: the gate fires after Hiro success + nonce release. The swap is preserved and the nonce is correctly marked consumed; the BlockedError is a contract signal to downstream consumers, not an attempted rollback of an on-chain tx.

Diego's observation about the checkpoint state inconsistency on TARGET_OUT_NOT_MET (checkpoint persists as nextRequiredAction: "Funding complete..." while CLI emits blocked) is accurate and worth a follow-up, but it's a UX edge, not a fund-safety issue.

✅ Item 2 — nonceManagerDeclared honesty

Was hardcoded true. Now tied to the same fileExists(path.join("skills", "nonce-manager", "nonce-manager.ts")) check as nonceManagerLocal. The comment at the change site correctly explains why: an absent file would have produced an opaque acquireNonce throw rather than a clean dependencies.nonceManagerDeclared: false in doctor output.

✅ Item 3 — Nonce alignment comment

Comment at the lock acquisition site is explicit and correct:

This lock prevents OUR concurrent writers from racing each other; it does NOT serialize against external writers. If the primitive becomes a nonce-manager participant, this caveat goes away.

That's the honest documentation of the current serialization scope. The open question (whether bitflow-swap-aggregator itself participates in nonce-manager) is now surfaced as a named caveat rather than silently assumed away.

✅ Item 4 — --mode validation

FUNDING_MODES = ["one-shot", "dca-chunk"] as const + resolveFundingMode() correctly throws INVALID_MODE upfront for unknown values. Applied at all three sites: newCheckpoint, fundingEnvelope, and the runResume synthesized checkpoint. dca-chunk validates but follows one-shot path — consistent with the deferred-per-PRD framing and correctly documented.

✅ Item 5 — Explicit-path extractors

extractExpectedOutput now traverses data.quote.quote → data.quote.expectedAmountOut → data.expectedAmountOut — no longer first-match DFS over arbitrary JSON.

extractOutputBalance explicitly prefers data.balancesAfter.outputBalance (post-write) over data.balances.outputBalance (pre-write). This was the actual ambiguity Diego flagged and it's cleanly resolved.

extractTxid correctly retains DFS — the 0x[0-9a-fA-F]{64} regex anchor makes first-match safe there.


Deferred items — still open, still tracked in #597

  • Checkpoint state vs error JSON mismatch on TARGET_OUT_NOT_MET (diegomey observation, non-blocking)
  • parseClarityOkUint coverage for non-uint-return swap functions → TARGET_OUT_UNVERIFIABLE is clean safe failure (diegomey observation, non-blocking)
  • Number precision for high-decimal tokens (future hardening, not reachable on proven STX/sBTC route)
  • Primitive nonce-manager participation (still an open question, now correctly documented)

Approving

All five #597 items implemented correctly. Mainnet proof landed at 0x766e90d3. Both blocking-item rounds are settled. Code is fund-safety correct at 7c268c8. Good to merge.


Review by arc0btc

@diegomey diegomey removed the DAY 28 label May 15, 2026
@diegomey diegomey merged commit 8557e23 into BitflowFinance:main May 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bitflow Bitflow Primitive Primitive winner-approved AIBTC x Bitflow Skills Pay The Bills Competition Winner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📑 PRD: bitflow-funding-coordinator

4 participants